// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Security.Principal;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using System.Web;

using Microsoft.TeamFoundation.Migration.Toolkit;
using Microsoft.TeamFoundation.Migration.Toolkit.Wit;
using Microsoft.Teamfoundation.Migration.VersionControl.SharePoint.Lists;
using Microsoft.Teamfoundation.Migration.VersionControl.SharePoint.UserGroup;
using Microsoft.TeamFoundation.Migration.WorkItemTracking.Wss.Generated;

namespace Microsoft.TeamFoundation.Migration.WorkItemTracking.Wss
{
    /// <summary>
    /// WSS helper object.
    /// </summary>
    class WssHelper : IDisposable
    {
        private bool m_isDisposed;                          // Disposed flag
        private string m_baseUrl;                           // Base Url
        private string m_taskList;                          // Name of the task list
        private List m_list;                                // List metadata
        private string m_listGuid;                          // Guid of the list
        private Lists m_listSvc;                            // List web service
        private WssValueMap m_userMap;                      // Maps user values from Wss to other systems

        // Max allowed length of attachment
        public const long MaxAttachmentLength = (long)Int32.MaxValue;

        /// <summary>
        /// Returns list service.
        /// </summary>
        public Lists ListService { get { return m_listSvc; } }

        /// <summary>
        /// Returns list guid.
        /// </summary>
        public string ListGuid { get { return m_listGuid; } }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="baseUrl">URL of the WSS server</param>
        /// <param name="taskList">Task list id</param>
        public WssHelper(string baseUrl, string taskList)
        {
            m_baseUrl = baseUrl;
            m_taskList = taskList;
            m_listSvc = new Lists(m_baseUrl);
            m_listSvc.Credentials = System.Net.CredentialCache.DefaultCredentials;

            m_list = GetList(m_taskList);
            m_listGuid = m_list.ID;
            m_userMap = GetUserMap();
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~WssHelper()
        {
            Dispose(false);
        }

        /// <summary>
        /// Saves tasks.
        /// </summary>
        /// <param name="tasks">Tasks to save</param>
        public void SaveTasks(params WssTask[] tasks)
        {
            XmlDocument doc = new XmlDocument();
            XmlElement batch = doc.CreateElement("Batch");
            batch.SetAttribute("OnError", "Continue");

            Dictionary<string, WssTask> taskMap = new Dictionary<string, WssTask>(StringComparer.InvariantCulture);

            for (int i = 0; i < tasks.Length; i++)
            {
                WssTask task = tasks[i];
                string methodId = Guid.NewGuid().ToString();
                string cmd = task.Id == null ? "New" : "Update";
                string cookie = string.Format(CultureInfo.InvariantCulture, "{0},{1}", methodId, cmd);

                taskMap.Add(cookie, task);

                XmlElement method = doc.CreateElement("Method");
                method.SetAttribute("ID", methodId);
                method.SetAttribute("Cmd", cmd);

                foreach (KeyValuePair<string, object> pair in task.Fields)
                {
                    if (!WssFields.IsReadOnly(pair.Key))
                    {
                        SubmitField(method, pair.Key, pair.Value);
                    }
                }

                if (task.Id != null)
                {
                    SubmitField(method, WssFields.Id, task.Id.Value);
                }
                batch.AppendChild(method);
            }

            // Submit updates.
            XmlNode resNode = m_listSvc.UpdateListItems(m_listGuid, batch);

            // Process results
            Results results = TranslateUpdateResults(resNode);

            for (int i = 0; i < results.Items.Length; i++)
            {
                ResultsResult res = results.Items[i];
                if (Convert.ToInt32(res.ErrorCode, 16) != 0)
                {
                    throw new WssStoreException(res.ErrorText);
                }
                WssTask task = taskMap[res.ID];
                task.Refresh(res.row);
            }
        }

        /// <summary>
        /// Deserializes results of updating tasks.
        /// </summary>
        /// <param name="resNode">Node with results</param>
        /// <returns>Results</returns>
        public Results TranslateUpdateResults(
            XmlNode resNode)
        {
            // Process results
            XmlSerializer xs = new XmlSerializer(typeof(Results));

            using (StringReader sr = new StringReader(resNode.OuterXml))
            {
                return (Results)xs.Deserialize(sr);
            }
        }

        /// <summary>
        /// Adds a single file the task.
        /// </summary>
        /// <param name="taskId">Task id</param>
        /// <param name="name">File name</param>
        /// <param name="content">Content of the file</param>
        public void AttachFile(string taskId, string name, byte[] content)
        {
            m_listSvc.AddAttachment(m_listGuid, taskId, name, content);
        }

        /// <summary>
        /// Removes files from the attachment.
        /// </summary>
        /// <param name="taskId">ID of the target task</param>
        /// <param name="urls">URLs of attachments that must be removed</param>
        public void RemoveFiles(string taskId, params string[] urls)
        {
            for (int i = 0; i < urls.Length; i++)
            {
                m_listSvc.DeleteAttachment(m_listGuid, taskId, urls[i]);
            }
        }

        /// <summary>
        /// Obtain attachments for the given task.
        /// </summary>
        /// <param name="taskId">Task id</param>
        /// <returns>Attachments</returns>
        public AttachmentsAttachment[] GetAttachments(string taskId)
        {
            XmlNode res = ListService.GetAttachmentCollection(ListGuid, taskId);
            XmlSerializer xs = new XmlSerializer(typeof(Attachments));
            AttachmentsAttachment[] attachments;
            using (StringReader sr = new StringReader(res.OuterXml))
            {
                attachments = ((Attachments)xs.Deserialize(sr)).Items;
                if (attachments == null)
                {
                    attachments = new AttachmentsAttachment[0];
                }
                return attachments;
            }
        }

        /// <summary>
        /// Finds task by id.
        /// </summary>
        /// <param name="id">Id of the task</param>
        /// <returns>Task with the given id</returns>
        public WssTask GetTask(string id)
        {
            string qry = string.Format(
                CultureInfo.InvariantCulture,
                "<Where><Eq><FieldRef Name='{0}' /><Value Type='Counter'>{1}</Value></Eq></Where>",
                WssFields.Id,
                id);

            return GetTasks(qry)[0];
        }

        /// <summary>
        /// Finds tasks described by the given query.
        /// </summary>
        /// <param name="queryInnerXml">Selection criteria</param>
        /// <returns>List of tasks</returns>
        public List<WssTask> GetTasks(string queryInnerXml)
        {
            //Create filter information
            XmlDocument doc = new XmlDocument();

            XmlElement query = doc.CreateElement("Query");
            query.InnerXml = queryInnerXml;

            XmlElement fields = GetFields(doc);
            XmlElement options = GetQueryOptions(doc, null);

            List<WssTask> tasks = new List<WssTask>();
            XmlSerializer xs = new XmlSerializer(typeof(listitems));

            for (; ; )
            {
                XmlNode node = m_listSvc.GetListItems(m_listGuid, null, query, fields, null, options);
                listitems li;
                using (StringReader sr = new StringReader(node.OuterXml))
                {
                    li = (listitems)xs.Deserialize(sr);
                }

                if (li.Items == null || li.Items.Length == 0 || li.Items[0].row == null)
                {
                    break;
                }

                for (int i = 0; i < li.Items[0].row.Length; i++)
                {
                    row row = li.Items[0].row[i];
                    WssTask task = new WssTask(this, row);
                    tasks.Add(task);
                }

                string pagingData = li.Items[0].ListItemCollectionPositionNext;
                if (string.IsNullOrEmpty(pagingData))
                {
                    break;
                }
                pagingData = HttpUtility.HtmlEncode(pagingData);
                options = GetQueryOptions(doc, pagingData);
            }
            return tasks;
        }

        #region IDisposable

        /// <summary>
        /// Disposes the object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
            m_listSvc.Dispose();
        }

        /// <summary>
        /// Disposes the object.
        /// </summary>
        /// <param name="disposing">Disposing flag</param>
        private void Dispose(bool disposing)
        {
            if (!m_isDisposed)
            {
                if (disposing)
                {
                    m_listSvc.Dispose();
                }
            }
        }

        #endregion

        /// <summary>
        /// Checks whether given user name is valid for WSS.
        /// </summary>
        /// <param name="name">Name</param>
        /// <returns>True if valid</returns>
        public bool IsUserValid(string name)
        {
            return m_userMap.OtherToWss.ContainsKey(name);
        }

        /// <summary>
        /// Checks whether given field is valid for WSS store.
        /// </summary>
        /// <param name="name">Field name</param>
        /// <returns>True if valid</returns>
        public bool IsFieldValid(string name)
        {
            return Array.IndexOf(WssFields.AllFields, name) != -1;
        }

        /// <summary>
        /// Creates query's fields element.
        /// </summary>
        /// <param name="doc">XML document</param>
        /// <returns>Fields element</returns>
        private XmlElement GetFields(XmlDocument doc)
        {
            XmlElement fields = doc.CreateElement("ViewFields");
            for (int i = 0; i < WssFields.AllFields.Length; i++)
            {
                string name = WssFields.AllFields[i];

                if (WssFields.IsQueryable(name))
                {
                    XmlElement field = doc.CreateElement("FieldRef");
                    field.SetAttribute("Name", WssFields.AllFields[i]);
                    fields.AppendChild(field);
                }
            }
            return fields;
        }

        /// <summary>
        /// Gets the query options to include paging.
        /// </summary>
        /// <param name="doc">XML document</param>
        /// <param name="pagingQuery">Paging info</param>
        /// <returns>Query options</returns>
        private XmlElement GetQueryOptions(XmlDocument doc, string pagingQuery)
        {
            if (!string.IsNullOrEmpty(pagingQuery))
            {
                pagingQuery = string.Format(
                    CultureInfo.InvariantCulture,
                    "<Paging ListItemCollectionPositionNext='{0}' />",
                    pagingQuery);
            }
            pagingQuery = string.Format(
                CultureInfo.InvariantCulture,
                "<DateInUtc>TRUE</DateInUtc>{0}",
                pagingQuery);

            XmlElement options = doc.CreateElement("QueryOptions");
            options.InnerXml = pagingQuery;
            return options;
        }

        /// <summary>
        /// Translates WSS account.
        /// </summary>
        /// <param name="account">WSS account</param>
        /// <returns>Regular account</returns>
        internal object TranslateWssAccount(string account)
        {
            if (string.IsNullOrEmpty(account))
            {
                return null;
            }
            int pos = account.LastIndexOf(";#");
            return account.Substring(pos + 2, account.Length - pos - 2);
        }

        /// <summary>
        /// Translates WSS date.
        /// </summary>
        /// <param name="date">WSS date</param>
        /// <returns>public date</returns>
        internal object TranslateWssDate(string date)
        {
            if (string.IsNullOrEmpty(date))
            {
                return null;
            }
            return Convert.ToDateTime(date, CultureInfo.InvariantCulture).ToLocalTime();
        }

        /// <summary>
        /// Translates WSS percent.
        /// </summary>
        /// <param name="percent">WSS percent</param>
        /// <returns>public percent</returns>
        internal object TranslateWssPercent(string percent)
        {
            if (string.IsNullOrEmpty(percent))
            {
                return null;
            }
            return Convert.ToString(
                (int)(XmlConvert.ToDecimal(percent) * 100), CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Submits field and its value into the batch.
        /// </summary>
        /// <param name="e">Element from the batch</param>
        /// <param name="field">Field name</param>
        /// <param name="value">Field value</param>
        internal void SubmitField(XmlElement e, string field, object value)
        {
            XmlElement fe = e.OwnerDocument.CreateElement("Field");
            fe.SetAttribute("Name", field);
            string stringVal;

            if (value == null || value is string && ((string)value).Length == 0)
            {
                stringVal = string.Empty;
            }
            else
            {
                switch (field)
                {
                    // Account fields
                    case WssFields.AssignedTo:
                    case WssFields.Editor:
                    case WssFields.Author:
                        stringVal = GetWssAccount(Convert.ToString(value, CultureInfo.InvariantCulture));
                        break;

                    case WssFields.StartDate:
                    case WssFields.DueDate:
                    case WssFields.Modified:
                    case WssFields.Created:
                        stringVal = GetWssDate(Convert.ToDateTime(value, CultureInfo.InvariantCulture));
                        break;

                    case WssFields.PercentComplete:
                        stringVal = GetWssPercent(Convert.ToInt32(value, CultureInfo.InvariantCulture));
                        break;

                    default:
                        stringVal = Convert.ToString(value, CultureInfo.InvariantCulture);
                        break;
                }
            }
            fe.InnerText = stringVal;
            e.AppendChild(fe);
        }

        /// <summary>
        /// Gets WSS account value for the public account.
        /// </summary>
        /// <param name="account">Public account</param>
        /// <returns>WSS account</returns>
        internal string GetWssAccount(string account)
        {
            return m_userMap.OtherToWss[account];
        }

        /// <summary>
        /// Gets WSS date for the public date.
        /// </summary>
        /// <param name="date">Public date</param>
        /// <returns>WSS date</returns>
        internal string GetWssDate(DateTime date)
        {
            return date.ToString("yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Gets WSS percent from the public percent.
        /// </summary>
        /// <param name="percent">Public percent</param>
        /// <returns>WSS percent</returns>
        internal string GetWssPercent(int percent)
        {
            return Convert.ToString(((decimal)percent) / 100, CultureInfo.InvariantCulture);
        }

        /// <summary>
        /// Gets a list with the given name.
        /// </summary>
        /// <param name="listDisplayName">List name</param>
        /// <returns>List object</returns>
        private List GetList(string listDisplayName)
        {
            XmlNode node = m_listSvc.GetList(listDisplayName);
            XmlSerializer xs = new XmlSerializer(typeof(List));
            using (StringReader sr = new StringReader(node.OuterXml))
            {
                return (List)xs.Deserialize(sr);
            }
        }

        /// <summary>
        /// Returns user map.
        /// </summary>
        /// <returns></returns>
        private WssValueMap GetUserMap()
        {
            UserGroup svc = new UserGroup(m_baseUrl);
            svc.Credentials = System.Net.CredentialCache.DefaultCredentials;
            XmlNode node = svc.GetUserCollectionFromSite();

            XmlSerializer xs = new XmlSerializer(typeof(GetUserCollectionFromSite));
            GetUserCollectionFromSite users;
            using (StringReader sr = new StringReader(node.OuterXml))
            {
                users = (GetUserCollectionFromSite)xs.Deserialize(sr);
            }

            WssValueMap userMap = new WssValueMap();
            for (int i = 0; i < users.Items.Length; i++)
            {
                userMap.AddMapping(users.Items[i].ID, users.Items[i].Name);
            }
            return userMap;
        }
    }
}
